React - 高级指引
代码分割
打包
- 大多数 React 应用都会使用 Webpack、Rollup、Browserify 这类构建工具打包文件
- 打包是一个将文件引入并合并到一个单独文件的过程,最终形成一个 “bundle”
代码分割
- 打包器 Rollup
- Webpack
- Browserify
- factor-bundle 能够创建多个包并在运行时动态加载
- 引入代码分割的最佳方式是动态 import()
1 | // Webpack 解析该语法时,会自动进行代码分割 |
- 使用 Babel 时,确保 Babel 能够解析动态 import 语法而不是将其转换。对于这一要求需要 babel-plugin-syntax-dynamic-import 插件
React.lazy
- React.lazy 函数能像渲染常规组件一样处理动态引入(的组件)
- 想要在使用服务端渲染的应用中使用,推荐 Loadable Components 这个库,它有一个很棒的服务端渲染打包指南
1 | import React, {Suspense} from 'react'; |
基于路由的代码分割
1 | // 使用 React.lazy 和 React Router 这类的第三方库,来配置基于路由的代码分割 |
Context
何时用 Context
- Context 提供一种在组件之间共享此类值的方式,不必显式地通过组件树逐层传递 props
- 应用场景:很多不同层级的组件需要访问同样的数据,
谨慎使用,因为这会使得组件的复用性变差
,包括管理当前的 locale、theme、或者一些缓存数据 - 如果只是想避免层层传递一些属性,组件组合(component composition)有时是比 context 更好的解决方案
1 | // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。 |
API
- React.createContext
1 | // React 渲染订阅Context 对象的组件,组件会从组件树中离自身最近匹配的 Provider 中读取到当前的 context 值 |
- Context.Provider
- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
- Provider 接收一个 value 属性,传递给消费组件
- 一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据
- value 变化时,它内部所有消费组件都会重新渲染
- Provider 及其内部 consumer 组件都不受制于
shouldComponentUpdate
函数,当 consumer 组件在其祖先组件退出更新的情况下也能更新
1 | <MyContext.Provider value={/* 某个值 */}> |
- Class.contextType
- 此属性可以让你使用 this.context 来获取最近 Context 上的值
1 | // 可以在任何生命周期中访问到它,包括 render 函数中 |
- Context.Consumer
1 | // 传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值,没有Provider 就是createContext的defaultValue |
- Context.displayName
- context 对象接受 displayName 的 property,字符串类型。React DevTools 使用该字符串来确定 context 要显示的内容
1
2
3
4
5const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
- context 对象接受 displayName 的 property,字符串类型。React DevTools 使用该字符串来确定 context 要显示的内容
- 动态 Context
- 在嵌套组件中更新 Context(通过 context 传递一个函数,使得消费组件更新 context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 确保传递给 createContext 的默认值数据结构是调用组件能匹配的
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
// 调用方
function ThemeTogglerButton() {
// Theme Toggler 按钮不仅仅只获取 theme 值,
// 它也从 context 中获取到一个 toggleTheme 函数
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme。。。
</button>
)}
</ThemeContext.Consumer>
);
}
- 在嵌套组件中更新 Context(通过 context 传递一个函数,使得消费组件更新 context)
- 注意
context
根据引用标识决定何时渲染(本质上是 value 属性值的浅比较),陷阱:provider 父组件进行重渲染时,可能会在消费组件中触发意外的渲染1
2
3
4
5
6
7
8
9
10
11// 每一次 Provider 重渲染时,value 属性总是被赋值为新的对象,会重新渲染下面所有的消费组件
// 改进:value 状态提升到父节点的 state
class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}> // value={this.state.value}
<Toolbar />
</MyContext.Provider>
);
}
}
- 错误边界
- 为了解决:部分UI的js错误导致整个应用崩溃
- 它是React 组件,可以捕获并打印发生在其子组件树任何位置的js错误,会渲染出备用 UI,而不是渲染那些崩溃了的子组件树
- 在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误
- 无法捕获的错误
- 事件处理
- 异步代码(setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
- class 组件定义了 static getDerivedStateFromError() 或 componentDidCatch() 任意一个(或两个)时,它就变成一个错误边界
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
} - 工作方式类似于js的 catch {},不同于错误边界只针对 React 组件
- 只有 class 组件才可以成为错误边界组件。大多数情况下, 只需要声明一次错误边界组件, 可以在整个应用中使用它
- 可以包装在最顶层的路由组件展示一个 “Something went wrong” 的错误信息,就像服务端框架处理崩溃一样
未捕获错误(Uncaught Errors)的新行为
- 任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载
组件栈追踪
- 渲染期间发生的所有错误打印到控制台,仅用于开发环境,
生产环境必须将其禁用
- 组件名称在栈追踪中的显示依赖于 Function.name 属性
- 如要支持 未提供该功能的旧版浏览器和设备(例如 IE 11),在打包(bundled)应用程序中包含一个 Function.name 的 polyfill(function.name-polyfill)或在所有组件上显式设置
displayName 属性
关于 try/catch
- 仅用于命令式代码
- 错误边界无法捕获事件处理器内部的错误 使用try/catch
- 错误边界保留了 React 的声明性质,例:即使错误发生在 componentDidUpdate 方法中,由某一个深层组件树的 setState 引起,仍然能冒泡到最近的错误边界
Refs
Refs 可以
访问DOM节点
、React元素
使用场景(勿过度使用)
- 处理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方DOM库
创建 Refs
1 | // React.create()创建,通过 ref 属性附加到元素上 |
- 访问 Refs,节点的类型影响ref的取值
- ref属性指向一个 DOM 元素或 class组件
- 不能在函数组件上使用ref属性,因为它没有实例
1 | function MyFunctionComponent() { |
- 在父组件中引用子节点的DOM节点,使用ref转发
转发 refs 到 DOM 组件
- Ref 转发是一个可选特性,组件可以像暴露自己的ref一样暴露子组件的ref
1 | const FancyButton = React.forwardRef((props, ref) => ( |
在高阶组件(HOC)中转发 refs
- ref 不是 属性,如果对 HOC 添加 ref,ref 会引用最外层的容器组件,而不是被包裹的组件
1 | function logProps(Component) { |
回调 Refs
- 内联函数定义的 ref 回调,更新执行两次,第一次传参为null,第二次才是DOM元素
- 因为每次渲染会创建新的函数实例,定义为class 绑定函数的方式可避免
1 | function CustomTextInput(props) { |
Fragments
- 一个组件返回多个元素,Fragments 将子列表分组,不用向 DOM 添加额外节点
1 | class Columns extends React.Component { |
高阶组件
- 基于 React 的组合特性而形成的设计模式
- 高阶组件是参数为组件,返回值为新组件的函数(组件转化为另一个组件),纯函数
- HOC 不会修改传入的组件,也不会使用继承来复制其行为
不要改变原始组件,使用组合
1 | function logProps(InputComponent) { |
将不相关的 props 传递给被包裹的组件
- (HOC 应该透传与自身无关的 props)[https://react.docschina.org/docs/higher-order-components.html]
最大化可组合性
- 最常见的HOC
1 | // React Redux 的 `connect` 函数 |
1 | // connect 是一个函数,它的返回值为另外一个函数。 |
不要在 render 方法中使用 HOC
1 | render() |
务必复制静态方法
- 新组件没有原始组件的任何静态方法
1 | // 在返回之前把这些方法拷贝到容器组件上 |
与第三方库协同
- React 不会理会 React 自身之外的 DOM 操作
- 它根据内部虚拟 DOM 来决定是否需要更新,如果同一个 DOM 节点被另一个库操作了,React 会觉得困惑而且没有办法恢复
- 避免冲突的最简单方式就是防止 React 组件更新(渲染无需更新的 React 元素,比如一个空的 )
性能优化
使用生产版本
虚拟化长列表
- 应用渲染长列表(上百甚至上千的数据)使用“虚拟滚动”
- 热门的虚拟滚动库:react-window
和 react-virtualized
避免调停
- 组件的
props
或state
变更,React会将最新返回的元素与之前渲染的元素对比,决定是否有必要更新真实的 DOM。如不相同,React 会更新该 DOM - React 只更新改变了的 DOM 节点,重新渲染仍然花费一些时间,如果很慢,可用
shouldComponentUpdate
提速,该方法会在重新渲染前被触发
1 | // 默认实现总是返回 true |
- 大部分情况可以继承
React.PureComponent
以代替手写 shouldComponentUpdate()
shouldComponentUpdate 的作用
1 | class CounterButton extends React.Component { |
1 | // 这个代替上面,仅做对象的浅比较,无法检查深层差别 |
- 避免上面问题:改变指针
1 | handleClick() |
1 | function updateColorMap(colormap) { |
Portals
- 将子节点渲染到父组件以外的DOM节点
1 | ReactDOM.createPortal(child, container) |
1 | render() |
- 应用:父组件有
overflow: hidden
或z-index
需要在视觉上跳出容器 - portal 存在于 React 树, 且与 DOM 树 中的位置无关
- 从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先
Profiler
- 可以添加在 React 树中的任何地方测量树中这部分渲染所带来的开销
- 两个prop: id(组件)、组件更新被调用的回调函数
1 | render( |
回调参数
1 | function onRenderCallback( |
没有使用ES6
- class 关键字定义组件,还可以使用
create-react-class
模块
1 | var createReactClass = require('create-react-class'); |
- 函数组件和class组件,都有defaultProps属性
1 | class Greeting extends React.Component { |
- 初始化 state
1 | class Counter extends React.Component { |
- createReactClass()创建组件,方法自动绑定到实例不需要在constructor中.bind(this)
Mixins
- es6本身不包含任何
mixin
,class组件不支持 - mixins(js对象:通过它封装通用的函数) 调用在组件之前
协调
Diffing 算法
对比不同类型的元素
:根结点为不同类型元素时,React会拆卸原有的树建起新的树。根节点以下的组件也会被卸载,它们的状态会被销毁对比同一类型的元素
:会保留DOM节点,仅对比更新有改变的属性对比同类型的组件元素
:组件实例不变,更新组件实例的props 跟 最新的元素保持一致对子节点进行递归
:递归DOM节点的子元素时,React会同时遍历两个子元素列表,产生差异时,生成一个mutation
1 | // 匹配完前两个最后插入第三个 |
keys
:为了解决以上消耗性能,React使用key匹配原有树上的子元素与最新树上的子元素- 避免使用index,有顺序修改,diff变慢
Render Props
- 用于告知组件需要渲染什么内容的函数prop
1 | // 追逐鼠标 |
1 | // 如果你出于某种原因真的想要 HOC,那么你可以轻松实现 |
1 | // 不必使用render也可以 |
Render Props
与React.PureComponent
避免一起使用,每一个 render 对于render prop总会生成新值,浅比较props的时候总会是false
1 | class MouseTracker extends React.Component { |
静态类型检查
- 运行前识别类型问题,使用
Flow
或TypeScript
来代替PropTypes
Flow
- 通过类型注释的特殊语法 扩展js,浏览器解析不了这种语法
- 编译后的代码去除 Flow 语法
TypeScript
- 在
tsconfig.json
中定义配置项 - .tsx 包含 JSX 代码的 ts文件
类型定义
- 判断一个库是否包含类型,
index.d.ts
或者 package.json 文件的typings
或types
属性中指定类型文件 DefinitelyTyped
为没有声明文件的js库提供类型定义
1 | // yarn |
- 局部声明:使用的包里没有声明文件,在 DefinitelyTyped 上也没有,创建本地定义文件,根目录declarations.d.ts 文件
1 | declare |
严格模式
StrictMode
显示应用程序中潜在问题的工具,不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告
1 | <React.StrictMode> |
识别不安全的生命周期
使用过时字符串 ref API 的警告
- createRef方式会警告,回调 ref 依旧适用
使用废弃的 findDOMNode 方法的警告
- 只读一次的 API:调用只会返回第一次查询的结果。子组件渲染不同的节点,无法跟踪更改
- findDOMNode使父组件需要单独渲染子组件,产生重构;
- 仅在组件返回单个且不可变的 DOM节点才有效
- ref传递、转发
检测意外的副作用
- React 工作的两个阶段
- 渲染:确定进行的更改(新旧DOM树对比)
- 渲染阶段的声明周期可能会被多次调用
- constructor
- componentWillMount (or UNSAFE_componentWillMount)
- componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
- componentWillUpdate (or UNSAFE_componentWillUpdate)
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- setState 更新函数(第一个参数)
- 提交:React 应用变化时(React DOM插入、更新、删除节点)调用生命周期方法
检测过时的 context API
使用 PropTypes 进行类型检查
- 限制单个元素
1 | import PropTypes from 'prop-types'; |
- 默认 Prop 值
- 类型检查适用 defaultProps,发生在它赋值后
1 | class Greeting extends React.Component { |
非受控组件
- 受控组件:表单数据React组件管理
- 非受控组件:表单数据由DOM节点处理(ref操作)
- 默认值
1 | <input |
<input type="file"/>
始终是一个非受控组件
Web Components
- 为可复用组件提供了强大的封装,而 React 提供了声明式的解决方案,使 DOM 与数据保持同步
1 | class XSearch extends HTMLElement { |